require "test_helper"

class Api::V1::GitHubSecretScanningTest < ActionDispatch::IntegrationTest
  HEADER_KEYID = "GITHUB-PUBLIC-KEY-IDENTIFIER".freeze
  HEADER_SIGNATURE = "GITHUB-PUBLIC-KEY-SIGNATURE".freeze

  DEPS_DEV_HEADER_KEYID = "DepsDev-Public-Key-Identifier".freeze
  DEPS_DEV_HEADER_SIGNATURE = "DepsDev-Public-Key-Signature".freeze

  KEYS_RESPONSE_BODY =
    { "public_keys" => [
      {
        "key_identifier" => "test_key_id",
        "is_current" => true
      }
    ] }.freeze

  [
    [:github, HEADER_KEYID, HEADER_SIGNATURE, GitHubSecretScanning::KEYS_URI],
    [:deps_dev, DEPS_DEV_HEADER_KEYID, DEPS_DEV_HEADER_SIGNATURE, GitHubSecretScanning::DepsDev::KEYS_URI]
  ].each do |name, header_keyid, header_signature, keys_uri|
    context "from #{name}" do
      context "on POST to revoke" do
        setup do
          key = OpenSSL::PKey::EC.generate("secp256k1")
          @private_key_pem = key.to_pem
          @public_key_pem = key.public_to_pem

          h = KEYS_RESPONSE_BODY.dup
          h["public_keys"][0]["key"] = @public_key_pem

          stub_request(:get, keys_uri)
            .to_return(
              status: 200,
              headers: { "Content-Type" => "application/json" },
              body: h.to_json
            )

          @tokens = [
            { "token" => "some_token", "type" => "some_type", "url" => "some_url" }
          ]

          @user = create(:user)
        end

        context "with no key_id" do
          setup do
            post revoke_api_v1_api_key_path(@rubygem),
              params: {},
              headers: { header_signature => "bar" }
          end

          should "deny access" do
            assert_response :unauthorized
            assert_match "Missing GitHub Signature", @response.body
          end
        end

        context "with no signature" do
          setup do
            post revoke_api_v1_api_key_path(@rubygem),
              params: {},
              headers: { header_keyid => "foo" }
          end

          should "deny access" do
            assert_response :unauthorized
            assert_match "Missing GitHub Signature", @response.body
          end
        end

        context "with invalid key_id" do
          setup do
            post revoke_api_v1_api_key_path(@rubygem),
              params: {},
              headers: { header_keyid => "foo", header_signature => "bar" }
          end

          should "deny access" do
            assert_response :unauthorized
            assert_match "Can't fetch public key from GitHub", @response.body
          end
        end

        context "with invalid signature" do
          setup do
            signature = sign_body("Hello world!")
            post revoke_api_v1_api_key_path(@rubygem),
              params: {},
              headers: { header_keyid => "test_key_id", header_signature => Base64.encode64(signature) }
          end

          should "deny access" do
            assert_response :unauthorized
            assert_match "Invalid GitHub Signature", @response.body
          end
        end

        context "without a valid token" do
          setup do
            signature = sign_body(JSON.dump(@tokens))
            post revoke_api_v1_api_key_path(@rubygem),
              params: @tokens,
              headers: { header_keyid => "test_key_id", header_signature => Base64.encode64(signature) },
              as: :json
          end

          should "returns success" do
            assert_response :success
            json = JSON.parse(@response.body)[0]

            assert_equal "false_positive", json["label"]
            assert_equal @tokens[0]["type"], json["token_type"]
            assert_equal @tokens[0]["token"], json["token_raw"]
          end
        end

        context "with a valid token" do
          setup do
            key = "rubygems_#{SecureRandom.hex(24)}"
            @api_key = create(:api_key, key: key)
            @tokens << { "token" => key, "type" => "rubygems", "url" => "some_url" }
            signature = sign_body(JSON.dump(@tokens))

            perform_enqueued_jobs only: ActionMailer::MailDeliveryJob do
              post revoke_api_v1_api_key_path(@rubygem),
                params: @tokens,
                headers: { header_keyid => "test_key_id", header_signature => Base64.encode64(signature) },
                as: :json
            end
          end

          should "returns success and remove the token" do
            assert_response :success

            json = JSON.parse(@response.body)

            assert_equal "true_positive", json.last["label"]
            assert_equal @tokens.last["token"], json.last["token_raw"]

            assert_predicate @api_key.reload, :expired?
          end

          should "delivers an email" do
            refute_empty ActionMailer::Base.deliveries
            email = ActionMailer::Base.deliveries.last

            assert_equal [@api_key.user.email], email.to
            assert_equal ["no-reply@mailer.rubygems.org"], email.from
            assert_equal "One of your API keys was revoked on rubygems.org", email.subject
            assert_match "some_url", email.body.to_s
          end
        end
      end
    end
  end

  private

  def sign_body(body)
    private_key = OpenSSL::PKey::EC.new(@private_key_pem)
    private_key.sign(OpenSSL::Digest.new("SHA256"), body)
  end
end
